
// Copyright (C) 2011 Luca Piccioni
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace Derm.Render
{
	using ObjectLibrary = Dictionary<string, Type>;

	using ProgramLibrary = Dictionary<string, Type>;

	/// <summary>
	/// Manage shaders library integrated with the ShaderGen tool.
	/// </summary>
	/// <remarks>
	/// <para>
	/// This class achieve the following objectives:
	/// - Collect available shader objects types and shader programs type, to allow shader creation.
	/// - Cache already compiled shader objects instances
	/// - Cache already linked shader program instances
	/// </para>
	/// <para>
	/// The collection of shader object types and shader program type are done by inspecting public types
	/// in specific assemblies. All types deriving from <see cref="ShaderObject"/> or <see cref="ShaderProgram"/>
	/// are candidates for collection.
	/// 
	/// 
	/// </para>
	/// </remarks>
	public static class ShaderLibrary
	{
		#region Constructors

		/// <summary>
		/// Static constructor.
		/// </summary>
		static ShaderLibrary()
		{
			foreach (ShaderObject.Stage stage in Enum.GetValues(typeof(ShaderObject.Stage)))
				sObjectLibraryByStage[stage] = new Dictionary<string,Type>();

			try {
				// Default shader library
				LoadShaders("Derm.Shaders.dll");
			} catch (Exception) {
				// Note: this assembly could partially run without the default shader library.
			}
		}

		/// <summary>
		/// Force static constructor invocation.
		/// </summary>
		internal static void Touch() { }

		#endregion

		#region Shaders Library Type Database

		internal static ShaderObject CreateShaderObject(RenderContext ctx, ShaderCompilerContext cctx, string identifier, ShaderObject.Stage stage)
		{
			if (ctx == null)
				throw new ArgumentNullException("ctx");
			if (cctx == null)
				throw new ArgumentNullException("cctx");
			if (identifier == null)
				throw new ArgumentNullException("identifier");

			Type objectType = null;
			object[] ctorArgs = null;

			if        (sObjectLibraryByStage[stage].ContainsKey(identifier) == true) {
				objectType = sObjectLibraryByStage[stage][identifier];
				ctorArgs = new object[0];
			} else if (sObjectLibraryGeneric.ContainsKey(identifier) == true) {
				objectType = sObjectLibraryGeneric[identifier];
				ctorArgs = new object[] { stage };
			}

			if (objectType == null)
				throw new ArgumentException("unknown "+ stage.ToString().ToLower() +" shader object " + identifier, "identifier");

			// Create first instance
			ShaderObject sObject = (ShaderObject)Activator.CreateInstance(objectType, ctorArgs);
			sObject.Create(ctx, cctx);

			return (sObject);
		}

		internal static ShaderProgram CreateShaderProgram(string identifier)
		{
			if (identifier == null)
				throw new ArgumentNullException("identifier");

			if (sProgramLibrary.ContainsKey(identifier) == false)
				throw new ArgumentException("unknown shader program " + identifier, "identifier");

			Type programType = sProgramLibrary[identifier];

			ShaderProgram program = (ShaderProgram)Activator.CreateInstance(programType);
			program.Create(new ShaderCompilerContext());

			return (program);
		}

		internal static ShaderProgram CreateShaderProgram(ShaderCompilerContext cctx, string identifier)
		{
			if (cctx == null)
				throw new ArgumentNullException("cctx");
			if (identifier == null)
				throw new ArgumentNullException("identifier");

			if (sProgramLibrary.ContainsKey(identifier) == false)
				throw new ArgumentException("unknown shader program " + identifier, "identifier");

			Type programType = sProgramLibrary[identifier];

			ShaderProgram program = (ShaderProgram)Activator.CreateInstance(programType);
			program.Create(cctx);

			return (program);
		}

		internal static ShaderProgram CreateShaderProgram(RenderContext ctx, ShaderCompilerContext cctx, string identifier)
		{
			if (ctx == null)
				throw new ArgumentNullException("ctx");
			if (cctx == null)
				throw new ArgumentNullException("cctx");
			if (identifier == null)
				throw new ArgumentNullException("identifier");

			if (sProgramLibrary.ContainsKey(identifier) == false)
				throw new ArgumentException("unknown shader program " + identifier, "identifier");

			Type programType = sProgramLibrary[identifier];

			ShaderProgram program = (ShaderProgram)Activator.CreateInstance(programType);
			program.Create(ctx, cctx);

			return (program);
		}

		/// <summary>
		/// Load shader objects and programs from an assembly.
		/// </summary>
		/// <param name="path">
		/// A <see cref="System.String"/> that specify the assembly path.
		/// </param>
		public static void LoadShaders(string path)
		{
			Assembly sLibrary = Assembly.LoadFile(Path.GetFullPath(path));
			Type[] shaderTypes = sLibrary.GetTypes();

			foreach (Type sType in shaderTypes) {
				try {
					if (sType.IsAbstract == true)
						continue;

					// Type checks
					if        (sType.IsSubclassOf(typeof(ShaderObject)) == true) {
						LoadShaderObject(sType);
					} else if (sType.IsSubclassOf(typeof(ShaderProgram)) == true) {
						LoadShaderProgram(sType);
					} else if (sType.GetInterface("IShaderInclude") != null) {
						LoadShaderInclude(sType);
					}
				} catch (Exception typeException) {
					throw new InvalidOperationException("unable to load", typeException);
				}
			}

			if (RenderContext.CurrentCaps.ShaderInclude.Supported == false) {
				foreach (Type includeType in sIncludeLibrary) {
					IShaderInclude shaderInclude = (IShaderInclude)Activator.CreateInstance(includeType);

					if (ShaderIncludeTree.ExistsShaderInclude(null, shaderInclude) == false)
						ShaderIncludeTree.CreateShaderInclude(null, shaderInclude);
				}
				sIncludeLibrary.Clear();
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public static IEnumerable<string> GetShaderLibraryIncludes()
		{
			return (ShaderIncludeTree.GetShaderIncludeTree());
		}

		public static IEnumerable<string>  GetShaderLibraryObjects()
		{
			List<string> objectList = new List<string>();

			foreach (string objectName in sObjectLibraryGeneric.Keys)
				objectList.Add(String.Format("{0} (generic)", objectName));
			foreach (ShaderObject.Stage stage in Enum.GetValues(typeof(ShaderObject.Stage))) {
				foreach (string objectName in sObjectLibraryByStage[stage].Keys)
					objectList.Add(String.Format("{0} ({1})", objectName, stage));
			}

			return (objectList);
		}

		public static IEnumerable<string>  GetShaderLibraryPrograms()
		{
			List<string> programList = new List<string>();

			foreach (string programName in sProgramLibrary.Keys)
				programList.Add(programName);

			return (programList);
		}

		/// <summary>
		/// Load a shader object type.
		/// </summary>
		/// <param name="sType">
		/// A <see cref="System.Type"/> which subclass <see cref="ShaderObject"/>.
		/// </param>
		/// <remarks>
		/// <para>
		/// 
		/// </para>
		/// </remarks>
		/// <exception cref="InvalidOperationException">
		/// Exception throw 
		/// </exception>
		private static void LoadShaderObject(Type sType)
		{
			ObjectLibrary sObjectLibrary = null;
			string sObjectLibraryId = ShaderObject.GetLibraryId(sType);
			bool hasDefaultStage = ShaderObject.HasLibraryStage(sType);
			bool ctorDefault = ShaderObject.HasDefaultConstructor(sType);
			bool ctorStage = ShaderObject.HasStageConstructor(sType);

			if        ((ctorDefault == true) && (ctorStage == false)) {
				ShaderObject.Stage sObjectStage;

				// Note: only default constructor implemented

				if (hasDefaultStage == true) {

					// Note: assumes that the default constructor specify the shader object stage
					// equals to 'sObjectStage' value

					sObjectStage = ShaderObject.GetLibraryStage(sType);
				} else {

					// Note: ShaderObject implementation doesn't declare a default stage, so here it is
					// asserted by creating an instance (default constructor is present)

					try {
						ShaderObject sObject = (ShaderObject)Activator.CreateInstance(sType);
						sObjectStage = sObject.ObjectStage;
					} catch (Exception e) {
						throw new InvalidOperationException(String.Format("unable to create instance of {0}", sType.FullName), e);
					}
				}
				// Shader object library is dependent on the shader object stage
				sObjectLibrary = sObjectLibraryByStage[sObjectStage];
			} else if ((ctorDefault == false) && (ctorStage == true)) {

				// Note: only constructor that specify object stage implemented

				// Shader object library is the generic one
				sObjectLibrary = sObjectLibraryGeneric;

			} else if ((ctorDefault == true) && (ctorStage == true)) {
				ShaderObject.Stage sObjectStage;

				// Note: both recognized constructors are implemented (ShaderObject with default)

				// Assert default constructor stage
				try {
					ShaderObject sObject = (ShaderObject)Activator.CreateInstance(sType);
					sObjectStage = sObject.ObjectStage;
				} catch (Exception e) {
					throw new InvalidOperationException(String.Format("unable to create instance of {0}", sType.FullName), e);
				}

				// Shader object library is dependent on the shader object stage
				sObjectLibrary = sObjectLibraryByStage[sObjectStage];
				// Process this type also as a generic one
				if (sObjectLibraryGeneric.ContainsKey(sObjectLibraryId) == true)
					throw new InvalidOperationException(String.Format("{0} identifier {1} conflict with current library", sType.FullName, sObjectLibraryId));
				sObjectLibraryGeneric[sObjectLibraryId] = sType;
			} else
				throw new InvalidOperationException(String.Format("{0} doesn't have required constructor(s)", sType.FullName));

			// Collect shader object type
			if (sObjectLibrary.ContainsKey(sObjectLibraryId) == true)
				throw new InvalidOperationException(String.Format("{0} identifier {1} conflict with current library", sType.FullName, sObjectLibraryId));
			sObjectLibrary[sObjectLibraryId] = sType;
		}

		private static void LoadShaderProgram(Type sType)
		{
			FieldInfo sProgramNameField = sType.GetField("LibraryId", BindingFlags.Static | BindingFlags.Public);

			// Field checks
			if (sProgramNameField == null)
				throw new InvalidOperationException("no ShaderProgramName public static field");
			if (sProgramNameField.FieldType != typeof(string))
				throw new InvalidOperationException("no ShaderProgramName is not a string");

			// Field value checks
			string sProgramName = (string)sProgramNameField.GetValue(null);

			if (String.IsNullOrEmpty(sProgramName) == true)
				throw new InvalidOperationException("ShaderProgramName field is null or empty");

			// Determine shader object stage
			ConstructorInfo sProgramDefaultCtor = sType.GetConstructor(System.Type.EmptyTypes);

			if (sProgramDefaultCtor == null)
				throw new InvalidOperationException("shader program doesn't have required constructor(s)");

			// Collect shader object type
			if (sProgramLibrary.ContainsKey(sProgramName) == true)
				throw new InvalidOperationException("ShaderProgram name conflict with current library");
			sProgramLibrary[sProgramName] = sType;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="sType"></param>
		private static void LoadShaderInclude(Type sType)
		{
			sIncludeLibrary.Add(sType);
		}

		/// <summary>
		/// 
		/// </summary>
		private static readonly Dictionary<ShaderObject.Stage, ObjectLibrary> sObjectLibraryByStage = new Dictionary<ShaderObject.Stage,ObjectLibrary>();

		/// <summary>
		/// 
		/// </summary>
		private static readonly ObjectLibrary sObjectLibraryGeneric = new ObjectLibrary();

		/// <summary>
		/// 
		/// </summary>
		private static readonly ProgramLibrary sProgramLibrary = new ProgramLibrary();

		/// <summary>
		/// 
		/// </summary>
		private static readonly List<Type> sIncludeLibrary = new List<Type>();

		#endregion
	}
}
